/*
<license>
Copyright (c) 2008, Chris Venter <chris.venter@gmail.com>
All rights reserved.

This file is part of quicktag <http://code.google.com/p/quicktag/>

quicktag is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.

quicktag is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this code.
If not, see <http://www.gnu.org/licenses/>.
</license>
*/
#ifndef ASF_FILE_H
#define ASF_FILE_H

/* \file asffile.h
This file implements a parser for ASF files.
It tries to extract all the metadata (well, the relevant ones for my needs)
from a given ASF file.

The code is based on Dan Sully's Audio::WMA perl code: 
http://search.cpan.org/~daniel/Audio-WMA-1.2/WMA.pm
which is a reverse engineering of the ASF spec, and released as part of the 
CPAN perl repository.

I have used the perl code to provide me with an algorithm template, on which I 
based my C++ code. Crude maybe, but very effective. This was easy given Dan's 
ultra clear and easy to read code. Thanks Dan!

Some parts of the original perl code is still embedded in the source as
comments. They should be preserved since they provide a good comment on what the
code is doing at any point in time.

The perl code is in the CPAN repository, and all code there falls under the 
same license as Perl's (http://dev.perl.org/licenses/).

I am therefore releasing this code under the GPL as well.

- Chris Venter 2008
<chris.venter@gmail.com>
*/

/*
The file open and read operations from fctnl.h are used.
glib.h's portable datatypes are required.
*/
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <glib.h>

#include <iostream>
#include <map>

#include <generik/stringutils.h>
#include <generik/read.h>

#include <generik/tracing.h>
//extern generik::tracing::tracer::stream;

#include <sys/types.h>
//typedef muint64 u_uint64_t; //long long unsigned int;
typedef 	uint64_t	muint64;

/** GUID class.
This class creates a GUID from a sequence of 16 bytes.
It can convert the GUID into a HEX-formatted string.
*/
struct guid
{
	guint32 a; // 4 bytes
	guint16 b; // 2 bytes
	guint16 c; // 2 bytes
	unsigned char d[8]; // 8 bytes
	// total: 16 bytes
	
	guid(const char* data)
	{
		const char* p = data;
		a = generik::read<guint32>(p);
		b = generik::read<guint16>(p);
		c = generik::read<guint16>(p);
		for (int i = 0; i < 8; ++i)
		{
			d[i] = generik::read<unsigned char>(p);
		}
	}

	operator std::string() const
	{
		char buf[128 + 1];
		sprintf(buf, "%08X-%04X-%04X-%02X%02X-%02X%02X%02X%02X%02X%02X",
			a, b, c, d[0], d[1], d[2], d[3], d[4], d[5], d[6], d[7]
		);
		std::string result = buf;
		return result;
	}
};
std::ostream& operator<<(std::ostream& stream, const guid& g)
{
	return stream<<std::string(g);
}

/*
The following definitions hold the string GUID values for specific objects which
may occur in an ASF header.
As far as I can tell only the ASF_Header_Object GUID 
(75B22630-668E-11CF-A6D9-00AA0062CE6C) 
is *required* since the ASF file must contain a valid header to be useful.
*/
#define ASF_Extended_Stream_Properties_Object	"14E6A5CB-C672-4332-8399-A96952065B5A"
#define ASF_Padding_Object			"1806D474-CADF-4509-A4BA-9AABCB96AAE8"
#define ASF_Payload_Ext_Syst_Pixel_Aspect_Ratio	"1B1EE554-F9EA-4BC8-821A-376B74E4C4B8"
#define ASF_Script_Command_Object		"1EFB1A30-0B62-11D0-A39B-00A0C90348F6"
#define ASF_No_Error_Correction			"20FB5700-5B55-11CF-A8FD-00805F5C442B"
#define ASF_Content_Branding_Object		"2211B3FA-BD23-11D2-B4B7-00A0C955FC6E"
#define ASF_Content_Encryption_Object		"2211B3FB-BD23-11D2-B4B7-00A0C955FC6E"
#define ASF_Digital_Signature_Object		"2211B3FC-BD23-11D2-B4B7-00A0C955FC6E"
#define ASF_Extended_Content_Encryption_Object	"298AE614-2622-4C17-B935-DAE07EE9289C"
#define ASF_Simple_Index_Object			"33000890-E5B1-11CF-89F4-00A0C90349CB"
#define ASF_Degradable_JPEG_Media		"35907DE0-E415-11CF-A917-00805F5C442B"
#define ASF_Payload_Extension_System_Timecode	"399595EC-8667-4E2D-8FDB-98814CE76C1E"
#define ASF_Binary_Media			"3AFB65E2-47EF-40F2-AC2C-70A90D71D343"
#define ASF_Timecode_Index_Object		"3CB73FD0-0C4A-4803-953D-EDF7B6228F0C"
#define ASF_Metadata_Library_Object		"44231C94-9498-49D1-A141-1D134E457054"
#define ASF_Reserved_3				"4B1ACBE3-100B-11D0-A39B-00A0C90348F6"
#define ASF_Reserved_4				"4CFEDB20-75F6-11CF-9C0F-00A0C90349CB"
#define ASF_Command_Media			"59DACFC0-59E6-11D0-A3AC-00A0C90348F6"
#define ASF_Header_Extension_Object		"5FBF03B5-A92E-11CF-8EE3-00C00C205365"
#define ASF_Media_Object_Index_Parameters_Obj	"6B203BAD-3F11-4E84-ACA8-D7613DE2CFA7"
#define ASF_Header_Object			"75B22630-668E-11CF-A6D9-00AA0062CE6C" // valid header object?
#define ASF_Content_Description_Object		"75B22633-668E-11CF-A6D9-00AA0062CE6C"
#define ASF_Error_Correction_Object		"75B22635-668E-11CF-A6D9-00AA0062CE6C"
#define ASF_Data_Object				"75B22636-668E-11CF-A6D9-00AA0062CE6C"
#define ASF_Web_Stream_Media_Subtype		"776257D4-C627-41CB-8F81-7AC7FF1C40CC"
#define ASF_Stream_Bitrate_Properties_Object	"7BF875CE-468D-11D1-8D82-006097C9A2B2"
#define ASF_Language_List_Object		"7C4346A9-EFE0-4BFC-B229-393EDE415C85"
#define ASF_Codec_List_Object			"86D15240-311D-11D0-A3A4-00A0C90348F6"
#define ASF_Reserved_2				"86D15241-311D-11D0-A3A4-00A0C90348F6"
#define ASF_File_Properties_Object		"8CABDCA1-A947-11CF-8EE4-00C00C205365"
#define ASF_File_Transfer_Media			"91BD222C-F21C-497A-8B6D-5AA86BFC0185"
#define ASF_Advanced_Mutual_Exclusion_Object	"A08649CF-4775-4670-8A16-6E35357566CD"
#define ASF_Bandwidth_Sharing_Object		"A69609E6-517B-11D2-B6AF-00C04FD908E9"
#define ASF_Reserved_1				"ABD3D211-A9BA-11cf-8EE6-00C00C205365"
#define ASF_Bandwidth_Sharing_Exclusive		"AF6060AA-5197-11D2-B6AF-00C04FD908E9"
#define ASF_Bandwidth_Sharing_Partial		"AF6060AB-5197-11D2-B6AF-00C04FD908E9"
#define ASF_JFIF_Media				"B61BE100-5B4E-11CF-A8FD-00805F5C442B"
#define ASF_Stream_Properties_Object		"B7DC0791-A9B7-11CF-8EE6-00C00C205365"
#define ASF_Video_Media				"BC19EFC0-5B4D-11CF-A8FD-00805F5C442B"
#define ASF_Audio_Spread			"BFC3CD50-618F-11CF-8BB2-00AA00B4E220"
#define ASF_Metadata_Object			"C5F8CBEA-5BAF-4877-8467-AA8C44FA4CCA"
#define ASF_Payload_Ext_Syst_Sample_Duration	"C6BD9450-867F-4907-83A3-C77921B733AD"
#define ASF_Group_Mutual_Exclusion_Object	"D1465A40-5A79-4338-B71B-E36B8FD6C249"
#define ASF_Extended_Content_Description_Object	"D2D0A440-E307-11D2-97F0-00A0C95EA850"
#define ASF_Stream_Prioritization_Object	"D4FED15B-88D3-454F-81F0-ED5C45999E24"
#define ASF_Payload_Ext_System_Content_Type	"D590DC20-07BC-436C-9CF7-F3BBFBF1A4DC"
#define ASF_Index_Object			"D6E229D3-35DA-11D1-9034-00A0C90349BE"
#define ASF_Bitrate_Mutual_Exclusion_Object	"D6E229DC-35DA-11D1-9034-00A0C90349BE"
#define ASF_Index_Parameters_Object		"D6E229DF-35DA-11D1-9034-00A0C90349BE"
#define ASF_Mutex_Language			"D6E22A00-35DA-11D1-9034-00A0C90349BE"
#define ASF_Mutex_Bitrate			"D6E22A01-35DA-11D1-9034-00A0C90349BE"
#define ASF_Mutex_Unknown			"D6E22A02-35DA-11D1-9034-00A0C90349BE"
#define ASF_Web_Stream_Format			"DA1E6B13-8359-4050-B398-388E965BF00C"
#define ASF_Payload_Ext_System_File_Name	"E165EC0E-19ED-45D7-B4A7-25CBD1E28E9B"
#define ASF_Marker_Object			"F487CD01-A951-11CF-8EE6-00C00C205365"
#define ASF_Timecode_Index_Parameters_Object	"F55E496D-9797-4B5D-8C8B-604DFE9BFB24"
#define ASF_Audio_Media				"F8699E40-5B4D-11CF-A8FD-00805F5C442B"
#define ASF_Media_Object_Index_Object		"FEB103F8-12AD-4C64-840F-2A1D2F7AD48C"
#define ASF_Alt_Extended_Content_Encryption_Obj	"FF889EF1-ADEE-40DA-9E71-98704BB928CE"

// utility parse macro
#define PARSE_IF(OBJECT) if (objectguidstr == OBJECT) { GENERIK_TRACE("parsing "#OBJECT); parse_##OBJECT(p); }

/** ASF file reader and parser class.
This class takes a filename as argument in its constructor, and proceeds to 
parse the specified file as if it were an ASF file.
The file is checked to be contain valid ASF structures.

As metadata is found in the file it is recorded and available for later use.
*/
struct asffile
{
	/// This flag is set to true if the file is found to be a valid ASF file.
	bool _valid;
	/// This member holds the size (in bytes) of the file.
	off_t _size;
	/// This member stores the status of the parse as a string.
	std::string _status;
	
	/** Determines the size of the opened file.
	Stores the value in the _size member.
	*/
	void getfilesize(int f)
	{
		GENERIK_TRACER;
		_size = lseek(f, 0, SEEK_END);
		lseek(f, 0, SEEK_SET);
		GENERIK_TRACE("actual size of file: "<<_size<<" bytes");
	}
	
	/** Constructor.
	Takes the filename of the potential ASF to open.
	*/
	asffile(const std::string& filename)
	: _valid(false)
	, _size(0)
	{
		GENERIK_TRACER;
		int file = open(filename.c_str(), O_RDONLY);
		if (file == -1)
		{
			GENERIK_ERROR("could not open "<<filename<<" for reading: "<<strerror(errno));
		}
		else
		{
			getfilesize(file);
			parseasfheader(file);
		}
		close(file);
	}
	
	/** Contents storage.
	This map stores the resulting Meta Information as key -> value pairs.
	*/
	std::map<std::string, std::string> contents;
	
	/** Returns the value of a key in the Contents map.
	Use this function to determine wether a key is present in the ASF meta info,
	and to retrieve its value.
	For example: To determine the Title of a ASF track, use
	\begincode
		#include <asffile.h>
		
		wma Wmaobject("someWMAfile.wma");
		std::string title = wma::get("Title");
	\endcode
	A list of possible keys are:
		- Title: The title of the content
		- WM/AlbumArtist: The artist mainly responsible for the content
		- WM/AlbumAlbum: The album to which it belongs
		- WM/TrackNumber: The index of the content on the album
		- WM/Genre: The genre of the content
		
	\todo More examples can be found here
	
	\todo Not all keys are being retrieved right now.
	This is still a work in progress; not all of Dan's original code has
	been implemented. Also, I don't need all of them in order for the class
	to be useful. This may change in future!
	*/
	bool get(const std::string& tag, std::string& strout)
	{
		GENERIK_TRACER;
		GENERIK_TRACE("looking for tag ["<<tag<<"]");
		std::map<std::string, std::string>::iterator i = contents.find(tag);
		if (i != contents.end())
		{
			strout = i->second;
			GENERIK_TRACE("found it with value ["<<strout<<"]");
			return true;
		}
		GENERIK_TRACE("not found");
		return false;
	}
	
	std::string get(const std::string& tag)
	{
		GENERIK_TRACER;
		std::string result;
		get(tag, result);
		return result;
	}
	
	bool has(const std::string& tag)
	{
		GENERIK_TRACER;
		GENERIK_TRACE("looking for tag ["<<tag<<"]");
		std::map<std::string, std::string>::iterator i = contents.find(tag);
		if (i != contents.end())
		{
			GENERIK_TRACE("found it");
			return true;
		}
		GENERIK_TRACE("not found");
		return false;
	}
	
	/** Removes nulls from a character array.
	This function uses the length to determine over which span of bytes the
	string lies, and then goes ahead and copies only the valid (ie. non-null) characters
	into the target string.
	\note target must have enough space to hold the result!
	Setting target to a character array of the same length is recommended.
	
	\note This function is really only a kludge - what we really need is a 
	better way to translate the UTF16 strings into UTF8. Uggghhh....
	*/
	void denull(char* target, const char* source, int length)
	{
		GENERIK_TRACER;
		GENERIK_TRACE("denulling "<<std::string(source, length));
		char* t = target;
		const char* s = source;
		int count = 0;
		while (count++ < length)
		{
			if (*s)
			{
				*t = *s;
				t++;
			}
			s++;
		}
		*t = 0;
		GENERIK_TRACE("result: "<<target);
	}
	
	/** Returns a string from a block of memory which is length size in bytes.
	Also denull()'s the resulting string.
	*/
	std::string get_string(const char*& data, int length)
	{
		GENERIK_TRACER;
		const char* valuep = generik::read(data, length);
		char buf[length + 1]; buf[length] = 0;
		denull(buf, valuep, length);
		std::string value = buf;
		//std::cout<<title<<": "<<title.length()<<std::endl;
		return value;
	}

	void parse_ASF_File_Properties_Object(const char*)
	{
		GENERIK_TRACER;
		GENERIK_TRACE("not implemented yet");
	}
	void parse_ASF_Stream_Properties_Object(const char*)
	{
		GENERIK_TRACER;
		GENERIK_TRACE("not implemented yet");
	}
	void parse_ASF_Header_Extension_Object(const char*)
	{
		GENERIK_TRACER;
		GENERIK_TRACE("not implemented yet");
	}
	
	/** Parser function for "Extended Content Description" objects.
	These objects contain extra information, such as
	album artist, album title, genre and so forth.
	They may also be accessed after processing with the get() command.
	*/
	void parse_ASF_Extended_Content_Description_Object(const char* data)
	{
		GENERIK_TRACER;
		
		const char* p = data;
		
		//$ext{'content_count'} = unpack('v', $self->_readAndIncrementOffset(2));
		gint16 content_count = generik::read<gint16>(p);
		GENERIK_TRACE("content_count: "<<content_count);
		
		for (int id = 0; id < content_count; ++id)
		{
			//$ext{'content'}->{$id}->{'name_length'}  = unpack('v', $self->_readAndIncrementOffset(2));
			guint16 name_length = generik::read<guint16>(p);
			GENERIK_TRACE(id<<" name_length: "<<name_length);
			
			//$ext{'content'}->{$id}->{'name'} = _denull( $self->_readAndIncrementOffset($ext{'content'}->{$id}->{'name_length'}) );
			std::string name = get_string(p, name_length);
			GENERIK_TRACE(id<<" name: ["<<name<<"]");
			
			//$ext{'content'}->{$id}->{'value_type'}   = unpack('v', $self->_readAndIncrementOffset(2));
			//$ext{'content'}->{$id}->{'value_length'} = unpack('v', $self->_readAndIncrementOffset(2));
			guint16 value_type   = generik::read<guint16>(p);
			guint16 value_length = generik::read<guint16>(p);
			GENERIK_TRACE("value type:   "<<value_type);
			GENERIK_TRACE("value length: "<<value_length);
			
			/*
			# Value types from ASF spec:
			# 0 = unicode string
			# 1 = BYTE array
			# 2 = BOOL (32 bit)
			# 3 = DWORD (32 bit)
			# 4 = QWORD (64 bit)
			# 5 = WORD (16 bit)
			*/
			//my $value = $self->_readAndIncrementOffset( $ext{'content'}->{$id}->{'value_length'} );
			const char* valuep = generik::read(p, value_length);
			
			if (value_type <= 1)
			{
				//$ext{'content'}->{$id}->{'value'} = _denull($value);
				std::string value = get_string(valuep, value_length);
				GENERIK_TRACE("string value: "<<value);
				contents[name] = value;
			}
			else if (value_type == 4)
			{
				//$ext{'content'}->{$id}->{'value'} = _parse64BitString($value);
				muint64 value = generik::read<muint64>(valuep);
				//muint64 value = parse64bitstr(valuep);
				GENERIK_TRACE("64 bit value: "<<value);
				contents[name] = generik::to_<std::string>(value);
			}
			else if (value_type == 2 || value_type == 3)
			{
				//$ext{'content'}->{$id}->{'value'} = unpack($ValTypeTemplates[ $ext{'content'}->{$id}->{'value_type'} ], $value);
				gint32 value = *(gint32*)valuep;
				GENERIK_TRACE("32 bit value: "<<value);
				contents[name] = generik::to_<std::string>(value);
			}
			else // value_type == 5
			{
				gint16 value = *(gint16*)valuep;
				GENERIK_TRACE("16 bit value: "<<value);
				contents[name] = generik::to_<std::string>(value);
			}
			
		}
		
	}
	
	/** Parser function for "Content Description" objects.
	These objects contain the main information, such as the title of the media.
	The key/values retrieved can be accessed using the get() function.
	*/
	void parse_ASF_Content_Description_Object(const char* data)
	{
		GENERIK_TRACER;
		//my @keys = qw(TITLE AUTHOR COPYRIGHT DESCRIPTION RATING);
		const char* p = data;
		
		//# populate the lengths of each key
		//for my $key (@keys) { $desc{"_${key}length"}	= unpack('v', $self->_readAndIncrementOffset(2)); }
		guint16 title_length = generik::read<guint16>(p);
		guint16 author_length = generik::read<guint16>(p);
		guint16 copyright_length = generik::read<guint16>(p);
		guint16 description_length = generik::read<guint16>(p);
		guint16 rating_length = generik::read<guint16>(p);
		
		//# now pull the data based on length
		//for my $key (@keys) {
		//	my $lengthKey  = "_${key}length";
		//	$desc{$key} = _UTF16ToUTF8($self->_readAndIncrementOffset($desc{$lengthKey}));
		//	delete $desc{$lengthKey};
		//}
		contents["Title"] 	= get_string(p, title_length);
		contents["Author"] 	= get_string(p, author_length);
		contents["Copyright"] 	= get_string(p, copyright_length);
		contents["Description"] = get_string(p, description_length);
		contents["Rating"] 	= get_string(p, rating_length);
		
		GENERIK_TRACE_VALUE(contents["Title"]);
		GENERIK_TRACE_VALUE(contents["Author"]);
		GENERIK_TRACE_VALUE(contents["Copyright"]);
		GENERIK_TRACE_VALUE(contents["Description"]);
		GENERIK_TRACE_VALUE(contents["Rating"]);
	}
	
	/** Object parse handler.
	This function looks for objects and parses them.
	*/
	void parseasfobjects(int f, int nobjects, int objectsize)
	{
		GENERIK_TRACER;
		
		//read($fh, $self->{'headerData'}, ($objectSize - 30));
		char headerdata[objectsize - 30];
		off_t offset = lseek(f, 0, SEEK_CUR);
		GENERIK_TRACE("file offset: "<<offset);
		read(f, &headerdata, objectsize - 30);
		
		const char* p = headerdata;
		
		for (int i = 0; i < nobjects; ++i)
		{
			GENERIK_TRACE("reading "<<i<<" of "<<nobjects);
			
			//my $nextObjectGUID     = $self->_readAndIncrementOffset(16);
			guid objectguid = generik::read<guid>(p);
			//my $nextObjectGUIDName = $reversedGUIDs{$nextObjectGUIDText};
			std::string objectguidstr = objectguid;
			GENERIK_TRACE("guid: "<<objectguidstr);
			
			//my $nextObjectSize = _parse64BitString($self->_readAndIncrementOffset(8));
			muint64 objectsize = generik::read<muint64>(p);
			GENERIK_TRACE("objectsize: "<<objectsize);
			
			//# dispatch on the different object types found:
			if (0);
			else PARSE_IF(ASF_File_Properties_Object)
			else PARSE_IF(ASF_Content_Description_Object)
			else PARSE_IF(ASF_Extended_Content_Description_Object)
			else PARSE_IF(ASF_Stream_Properties_Object)
			else PARSE_IF(ASF_Header_Extension_Object)
			else
			{
				GENERIK_TRACE("unhandled object");
				//std::cout<<"unhandled object"<<std::endl;
				// unhandled object!
				// check wether it is one of the specified ones?
				// if not the file is not valid - corrupt?
			}
			
			//$self->{'offset'} += ($nextObjectSize - 16 - 8);
			p += objectsize - 16 - 8;
		}
	}

	/** Header parse function.
	This function parses the information contained in the header file and
	checks it for validation.
	*/
	void parseasfheader(int f)
	{
		GENERIK_TRACER;
		
		//read($fh, my $headerObjectData, 30) or return -1;
		char headerobjectdata[30];
		read(f, &headerobjectdata, 30);
		
		const char* p = headerobjectdata;
		
		//my $objectId	  = substr($headerObjectData, 0, 16);
		const char* objectid = generik::read(p, 16);
		guid objectguid(objectid);
		GENERIK_TRACE("guid: "<<objectguid);
		if (std::string(objectguid) != ASF_Header_Object)
		{
			_status = "Not a valid wma file: invalid ASF_Header_Object";
			return;
		}
		
		//my $objectSize    = unpack('V', substr($headerObjectData, 16, 8) );
		guint32 objectsize = generik::read<guint32>(p, 8);
		GENERIK_TRACE("objectsize : "<<objectsize);
		
		//my $headerObjects = unpack('V', substr($headerObjectData, 24, 4));
		guint32 nobjects = generik::read<guint32>(p);
		GENERIK_TRACE("nobjects   : "<<nobjects);
		
		//my $reserved1     = vec(substr($headerObjectData, 28, 1), 0, 4);
		//my $reserved2     = vec(substr($headerObjectData, 29, 1), 0, 4);
		
		//# some sanity checks
		//return -1 if ($objectSize > $self->{'size'});
		// not sure - seems to indicate an error/warning when objectsize is larger than the filesize
		if (objectsize > _size)
		{
			GENERIK_TRACE("objectsize is bigger than filesize: file is either corrupt or not a WMA");
			return;
		}
	
		// according to me, the file is valid at this point!
		GENERIK_TRACE("according to parseasfheader(), the file is valid at this point");
		
		_valid = true;
		
		parseasfobjects(f, nobjects, objectsize);
	}
	
	/** Returns true if this instance holds valid ASF information.
	Use it to check wether a parse of a particular file was succesful.
	Use getstatus() to determine what went wrong/right during parsing.
	*/
	bool isvalid()
	{
		return _valid;
	}
	
	/** Returns the status of the parse as a string.
	This can indicate success or failure - to determine which use isvalid().
	*/
	std::string getstatus()
	{
		return _status;
	}
	
};


#endif




